using System;
using System.Collections.Generic;
using System.Xml;
using System.Collections.ObjectModel;
using System.ServiceModel.Syndication;

namespace ServiceModel.Syndication.LEAP2A {
    /// <summary>
    /// Strongly typed extension System.ServiceModel.Syndication Atom feeds
    /// which handles literals used by LEAP2A and provide LEAP helper methods
    /// </summary>
    public class LeapItem : SyndicationItem {
        /// <summary>
        /// Reference to the LeapFeed, needed to provide access the namespaces for CURIE links 
        /// </summary>
        protected LeapFeed feed;

        /// <summary>
        /// The LEAP2A type of this entry
        /// </summary>
        private string itemType;

        /// <summary>
        /// Dates used as part of a entry
        /// </summary>
        private readonly List<LeapDate> dateCollection = new List<LeapDate>();

        /// <summary>
        /// Collection of data for the entry, only used for person entries
        /// </summary>
        private readonly List<LeapDataItem> personalData = new List<LeapDataItem>();

        /// <summary>
        /// Collection of data for the entry, only used for organisation entries
        /// </summary>
        private readonly List<LeapDataItem> orgData = new List<LeapDataItem>();

        /// <summary>
        /// Collection of places associated with the entry,
        /// can be a simple string or a structured address.
        /// </summary>
        private readonly List<LeapSpatial> leapSpatial = new List<LeapSpatial>();

        /// <summary>
        /// IDs of entries that this entry is a reply to, used for tracking comments
        /// </summary>
        private readonly List<string> inReplyTo = new List<string>();

        /// <summary>
        /// IDs of entries which comment on this entry
        /// </summary>
        private readonly List<string> comments = new List<string>();

        /// <summary>
        /// Initializes a new instance of the <see cref="LeapItem"/> class.
        /// </summary>
        /// <param name="feed">The feed this item belongs to.</param>
        public LeapItem(LeapFeed feed) {
            this.feed = feed;
        }

        /// <summary>
        /// Gets and sets the ID of the syndication item, 
        /// shadows the Id provided by SyndicationItem to provide CURIE support.
        /// The Id should always be accessed through LeapItem or a dervied class
        /// to ensure consistency.
        /// </summary>
        /// <value></value>
        /// <returns>The ID of the syndication item.</returns>
        public new string Id {
            get { return feed.UriFromCurie(base.Id); }
            set { base.Id = feed.UriToCurie(value); }
        }

        /// <summary>
        /// Gets or sets the amount of the time spent on this activity, 
        /// see http://wiki.cetis.ac.uk/2009-03/LEAP2A_literals#activetime.
        /// </summary>
        /// <value>A LeapTimeSpan with the amount of time spent.</value>
        public LeapTimeSpan LeapActiveTime {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the LEAP2 type of Atom entry.
        /// </summary>
        /// <value>The type of the entry, standard LEAP clases are available as static elements of LeapClasses, 
        /// also see http://wiki.cetis.ac.uk/2009-03/LEAP2A_categories.</value>
        public LeapTypes LeapType {
            get { return LeapTypeValues.FromUrl(itemType, feed); }
            set { itemType = LeapTypeValues.UrlFor(value, feed); }
        }

        /// <summary>
        /// Gets or sets the LEAP2 type of Atom entry.
        /// </summary>
        /// <value>The type of the entry, standard LEAP clases are available as static elements of LeapClasses, 
        /// also see http://wiki.cetis.ac.uk/2009-03/LEAP2A_categories.</value>
        public string CustomLeapType {
            get { return itemType; }
            set { itemType = feed.UriFromCurie(value); }
        }

        /// <summary>
        /// Gets the collection of dates for this entry.
        /// </summary>
        /// <value>A collection of LeapDate items.</value>
        public List<LeapDate> LeapDates {
            get { return this.dateCollection; }
        }

        /// <summary>
        /// Gets the LEAP2A spatial values for this entry.
        /// </summary>
        /// <value>The location.</value>
        public List<LeapSpatial> LeapSpatial {
            get { return this.leapSpatial; }
        }

        /// <summary>
        /// Gets the personal data for this entry.
        /// </summary>
        /// <value>The personal data.</value>
        public List<LeapDataItem> PersonalData {
            get { return personalData; }
        }

        /// <summary>
        /// Gets the organisational data for this entry.
        /// </summary>
        /// <value>The organisation data.</value>
        public List<LeapDataItem> OrganisationData {
            get { return orgData; }
        }

        public string RoleId {
            get; set;
        }

        public string MyRole {
            get; set;
        }

        /// <summary>
        /// Gets the comments for this entry.
        /// </summary>
        /// <value>The comments.</value>
        public ICollection<LeapItem> Comments {
            get {
                List<LeapItem> commentEntries = new List<LeapItem>(comments.Count);

                for (int i = 0; i < comments.Count; i++) {
                    LeapItem comment = feed.LookupEntry(comments[i]);

                    if (comment != null) {
                        commentEntries.Add(comment);
                    }
                }

                return new ReadOnlyCollection<LeapItem>(commentEntries); 
            }
        }

        /// <summary>
        /// Gets a value indicating whether this entry is a comment.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this entry is a comment; otherwise, <c>false</c>.
        /// </value>
        public bool IsComment {
            get { return (inReplyTo.Count > 0); }
        }

        /// <summary>
        /// Adds another entry as a comment.
        /// </summary>
        /// <param name="item">The comment.</param>
        public void AddComment(LeapItem item) {
            RemoveComment(item);

            this.comments.Add(feed.UriFromCurie(item.Id));
            item.inReplyTo.Add(feed.UriFromCurie(this.Id));
        }

        /// <summary>
        /// Removes the a comment entry.
        /// </summary>
        /// <param name="item">The comment.</param>
        public void RemoveComment(LeapItem item) {
            this.comments.Remove(feed.UriToCurie(item.Id));
            this.comments.Remove(feed.UriFromCurie(item.Id));

            item.inReplyTo.Remove(feed.UriToCurie(this.Id));
            item.inReplyTo.Remove(feed.UriFromCurie(this.Id));
        }

        /// <summary>
        /// Recreate comment collections after loading a LEAP2A feed.
        /// </summary>
        internal void FixComments() {
            string[] ids = inReplyTo.ToArray();

            foreach (string id in ids) {
                LeapItem entry = feed.LookupEntry(id);

                if (entry != null) {
                    entry.AddComment(this);
                }
            }

            List<LeapLink> links = new List<LeapLink>();

            foreach (LeapLink link in this.Links) {
                links.Add(link);
            }

            foreach (LeapLink link in links) {
                if (link.RelationshipType == "replies") {
                    LeapItem entry = feed.LookupEntry(link.Uri);

                    if (entry != null) {
                        this.AddComment(entry);
                    }

                    this.Links.Remove(link);
                }
            }
        }

        /// <summary>
        /// Finds a date which matches the specified point, 
        /// optionally allow any date to be returned if an exact match is not found.
        /// </summary>
        /// <param name="point">The type of date desired.</param>
        /// <param name="any">if set to <c>true</c> the first date is returned if an exact match is not found.</param>
        /// <returns>A matched LeapDate or null</returns>
        public LeapDate FindDate(LeapDatePoints point, bool any) {
            for (int i = 0; i < this.dateCollection.Count; i++) {
                if (this.dateCollection[i].Point == point) {
                    return this.dateCollection[i];
                }
            }

            if (any && this.dateCollection.Count > 0) {
                return this.dateCollection[0];
            }

            return null;
        }

        /// <summary>
        /// Gets the leap category value for a given scheme or null if the scheme is not found.
        /// </summary>
        /// <param name="scheme">The scheme.</param>
        /// <returns>The category term or null</returns>
        public string FindLeapCategory(string scheme) {
            foreach (LeapCategory category in this.Categories) {
                if (category.Scheme == feed.LeapCategoryNamespace + scheme) {
                    return category.Name;
                }
            }

            return null;
        }

        /// <summary>
        /// Links the current LeapItem to another link item,
        /// links are made in the "has" direction, this is usally parent to child.
        /// A reverse link is automatically added.
        /// </summary>
        /// <param name="linkType">Type of the link.</param>
        /// <param name="entryToLink">The entry to link.</param>
        public void Has(LeapLinkPair linkType, LeapItem entryToLink) {
            Has(linkType, entryToLink, -1, DateTime.MinValue);
        }

        /// <summary>
        /// Links the current LeapItem to another link item,
        /// links are made in the "has" direction, this is usally parent to child.
        /// A reverse link is automatically added.
        /// </summary>
        /// <param name="linkType">Type of the link.</param>
        /// <param name="entryToLink">The entry to link.</param>
        /// <param name="ordinate">The position of this item in an ordered list.</param>
        public void Has(LeapLinkPair linkType, LeapItem entryToLink, int ordinate) {
            Has(linkType, entryToLink, ordinate, DateTime.MinValue);
        }

        /// <summary>
        /// Links the current LeapItem to another link item,
        /// links are made in the "has" direction, this is usally parent to child.
        /// A reverse link is automatically added.
        /// </summary>
        /// <param name="linkType">Type of the link.</param>
        /// <param name="entryToLink">The entry to link.</param>
        /// <param name="date">The date associated with this link, 
        /// usually used for date ordered content i.e. blog posts in a selection.</param>
        public void Has(LeapLinkPair linkType, LeapItem entryToLink, DateTime date) {
            Has(linkType, entryToLink, -1, date);
        }

        /// <summary>
        /// Links the current LeapItem to another link item,
        /// links are made in the "has" direction, this is usally parent to child.
        /// A reverse link is automatically added.
        /// </summary>
        /// <param name="linkType">Type of the link.</param>
        /// <param name="entryToLink">The entry to link.</param>
        /// <param name="ordinate">The position of this item in an ordered list.</param>
        /// <param name="date">The date associated with this link, 
        /// usually used for date ordered content i.e. blog posts in a selection.</param>
        public void Has(LeapLinkPair linkType, LeapItem entryToLink, int ordinate, DateTime date) {
            LeapLink link = new LeapLink(feed, new Uri(entryToLink.Id));
            link.LeapRelationshipType = linkType.Value;
            
            if (ordinate >= 0) {
                link.Order = ordinate;
            }

            if (date > DateTime.MinValue) {
                link.Date = date;
            }

            this.Links.Add(link);

            if (linkType != LeapLinkPair.Relation && linkType != LeapLinkPair.Related) {
                link = new LeapLink(feed, new Uri(this.Id));
                link.LeapRelationshipType = linkType.Inverse;

                if (ordinate >= 0) {
                    link.Order = ordinate;
                }

                if (date > DateTime.MinValue) {
                    link.Date = date;
                }

                entryToLink.Links.Add(link);
            }
        }

        /// <summary>
        /// Gets entries linked to an entry by link type,
        /// the link type to search for should be the type on the near side of the link.
        /// E.g. to find entries which are part of the current entry the link type
        /// would be LeapPredicates.IsPartOf.
        /// </summary>
        /// <param name="linkType">Type of the link.</param>
        /// <returns>
        /// A list of Atom entries filtered by link type
        /// </returns>
        public IList<LeapItem> GetLinkedByLinkType(string linkType) {
            return GetLinkedEntries(linkType, null);
        }

        /// <summary>
        /// Gets entries linked to an entry by link type,
        /// the link type to search for should be the type on the near side of the link.
        /// E.g. to find entries which are part of the current entry the link type
        /// would be LeapPredicates.IsPartOf.
        /// </summary>
        /// <param name="linkType">Type of the link.</param>
        /// <returns>
        /// A list of Atom entries filtered by link type
        /// </returns>
        public IList<LeapItem> GetLinkedByLinkType(LeapPredicates linkType) {
            return GetLinkedEntries(LeapPredicateValues.UrlFor(linkType, feed), null);
        }

        /// <summary>
        /// Gets entries linked to an entry by entry type, 
        /// the links on the current entry are followed and the linked entries 
        /// matching the requested type are returned.
        /// </summary>
        /// <param name="entryType">Type of the entry.</param>
        /// <returns>
        /// A list of Atom entries filtered by entry type
        /// </returns>
        public IList<LeapItem> GetLinkedByEntryType(string entryType) {
            return GetLinkedEntries(null, entryType);
        }

        public IList<LeapItem> GetLinkedByEntryType(LeapTypes entryType) {
            return GetLinkedEntries(null, LeapTypeValues.UrlFor(entryType, feed));
        }

        public IList<LeapItem> GetLinkedEntries(LeapPredicates linkType, LeapTypes entryType) {
            return GetLinkedEntries(LeapPredicateValues.UrlFor(linkType, feed), LeapTypeValues.UrlFor(entryType, feed));
        }

        /// <summary>
        /// Gets entries linked to an entry by link and type,
        /// the links on the current entry which have a relationship type 
        /// matching the link type and reference an entry matching the entry
        /// type are returned.
        /// E.g. the find goals which are part of the current entry the
        /// link type would be LeapPredicates.IsPartOf and the entry type would be 
        /// LeapClasses.Goal
        /// </summary>
        /// <param name="linkType">Type of the link.</param>
        /// <param name="entryType">Type of the entry.</param>
        /// <returns>
        /// A list of Atom entries filtered by link and entry type
        /// </returns>
        public IList<LeapItem> GetLinkedEntries(string linkType, string entryType) {
            IList<LeapItem> links = new List<LeapItem>(this.Links.Count);

            foreach (SyndicationLink link in this.Links) {
                LeapLink leapLink = link as LeapLink;

                if (leapLink != null) {
                    if (linkType == null || leapLink.RelationshipType == linkType) {
                        // Relation matches desired link type or no type specified
                        LeapItem entry = feed.LookupEntry(leapLink.Uri);

                        if (entry != null) {
                            // Entry found
                            if (entryType != null) {
                                if (entry.LeapType != null) {
                                    // Extension found, check the type
                                    if (entry.CustomLeapType == entryType) {
                                        links.Add(entry);
                                    }
                                }
                            } else {
                                // No entry type specified, add the linked entry
                                links.Add(entry);
                            }
                        }
                    }
                }
            }

            return links;
        }

        /// <summary>
        /// Creates a new <see cref="T:System.ServiceModel.Syndication.SyndicationLink"/> instance.
        /// Overrides the default SyndicationLink with a LeapLink
        /// </summary>
        /// <returns>
        /// A new <see cref="T:System.ServiceModel.Syndication.SyndicationLink"/> instance.
        /// </returns>
        protected override SyndicationLink CreateLink() {
            return new LeapLink(feed);
        }

        protected override SyndicationCategory CreateCategory() {
            return new LeapCategory(feed);
        }

        /// <summary>
        /// Attempts to parse LEAP2A element extensions from an Atom entry.
        /// </summary>
        /// <param name="reader">The <see cref="T:System.Xml.XmlReader"/> to read from.</param>
        /// <param name="version">The syndication version to use while parsing.</param>
        /// <returns>
        /// A value that specifies whether the element extension was parsed successfully.
        /// </returns>
        /// <exception cref="T:System.Xml.XmlException">Invalid XML encountered during read.</exception>
        protected override bool TryParseElement(XmlReader reader, string version) {
            switch (reader.NamespaceURI) {
                case LeapNamespaces.Rdf:
                    TryParseRdfType(reader);
                    MoveToNextElement(reader);
                    break;
                case LeapNamespaces.AtomThreading:
                    TryParseThreadedReply(reader);
                    MoveToNextElement(reader);
                    break;
                default:
                    if (LeapPredicateValues.ContainsNamespace(reader.NamespaceURI)) {
                        TryParseLeapElement(reader);
                        MoveToNextElement(reader);
                    } else {
                        return base.TryParseElement(reader, version);
                    }

                    break;
            }

            return true;
        }

        private static void MoveToNextElement(XmlReader reader) {
            if (reader.IsEmptyElement) {
                reader.Read();
            } else {
                while (reader.NodeType != XmlNodeType.EndElement) {
                    reader.Read();
                }

                if (reader.NodeType == XmlNodeType.EndElement) {
                    reader.Read();
                }
            }

            reader.MoveToElement();
        }

        private bool TryParseLeapElement(XmlReader reader) {
            switch (reader.LocalName) {
                case "activetime":
                    LeapTimeSpan ts = new LeapTimeSpan();
                    if (ts.TryParseTimeSpan(reader, this.PublishDate.DateTime)) {
                        LeapActiveTime = ts;
                        return true;
                    }

                    return false;
                case "date":
                    LeapDate dateItem = new LeapDate();
                    if (dateItem.TryParseLeapDate(reader)) {
                        LeapDates.Add(dateItem);
                        return true;
                    }

                    return false;
                case "spatial":
                    LeapSpatial spatial = new LeapSpatial();
                    if (spatial.TryParseLeapSpatial(reader)) {
                        LeapSpatial.Add(spatial);
                        return true;
                    }

                    return false;
                case "persondata":
                case "orgdata":
                    LeapDataItem data = LeapDataItem.TryParseDataItem(reader);
                    if (data != null) {
                        if (reader.LocalName == "orgdata") {
                            OrganisationData.Add(data);
                        } else {
                            PersonalData.Add(data);
                        }
                        return true;
                    }

                    return false;
                case "myrole":
                    this.MyRole = GetTextContent(reader);
                    return true;
                case "roleid":
                    this.RoleId = GetTextContent(reader);
                    return true;
            }

            return false;
        }

        /// <summary>
        /// Gets the content of an element from the XmlReader.
        /// </summary>
        /// <param name="reader">The reader.</param>
        /// <returns></returns>
        internal static string GetTextContent(XmlReader reader) {
            string content = null;

            if (!reader.IsEmptyElement) {
                while (reader.NodeType != XmlNodeType.EndElement) {
                    reader.Read();

                    if (reader.NodeType == XmlNodeType.Text) {
                        content = reader.Value;
                    }
                }
            }

            return content;
        }

        /// <summary>
        /// Tries to parse type of entry from an RDF type element.
        /// </summary>
        /// <param name="reader">The reader.</param>
        /// <returns></returns>
        private bool TryParseRdfType(XmlReader reader) {
            if (reader.LocalName == "type") {
                string tempRdfType = reader.GetAttribute(reader.Prefix + ":resource");
                string tempRdfType2 = GetTextContent(reader);

                if (string.IsNullOrEmpty(tempRdfType)) {
                    tempRdfType = tempRdfType2;
                }

                if (!string.IsNullOrEmpty(tempRdfType)) {
                    this.CustomLeapType = tempRdfType;                    

                    return true;
                }
            }

            return false;
        }

        private bool TryParseThreadedReply(XmlReader reader) {
            if (reader.LocalName == "in-reply-to") {
                string parent = reader.GetAttribute("ref");

                if (!string.IsNullOrEmpty(parent)) {
                    this.inReplyTo.Add(parent);

                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Writes LEAP elements to an XmlWriter.
        /// </summary>
        /// <param name="writer">The XmlWriter.</param>
        /// <param name="version">The syndication version to use while writing.</param>
        protected override void WriteElementExtensions(XmlWriter writer, string version) {
            if (!string.IsNullOrEmpty(this.CustomLeapType)) {
                writer.WriteStartElement("rdf", "type", LeapNamespaces.Rdf);
                writer.WriteAttributeString("rdf", "resource", LeapNamespaces.Rdf, feed.UriToCurie(this.CustomLeapType));
                writer.WriteEndElement();
            }

            if (!string.IsNullOrEmpty(this.MyRole)) {
                writer.WriteElementString(feed.LeapPredicatesNamespacePrefix, "myrole", feed.LeapPredicatesNamespace, this.MyRole);
            }

            if (!string.IsNullOrEmpty(this.RoleId)) {
                writer.WriteElementString(feed.LeapPredicatesNamespacePrefix, "roleid", feed.LeapPredicatesNamespace, this.RoleId);
            }

            foreach (string commentId in comments) {
                writer.WriteStartElement("link");
                writer.WriteAttributeString("rel", "replies");
                writer.WriteAttributeString("href", feed.UriToCurie(commentId));
                writer.WriteEndElement();
            }

            foreach (string commentId in inReplyTo) {
                writer.WriteStartElement("thr", "in-reply-to", LeapNamespaces.AtomThreading);
                writer.WriteAttributeString("ref", feed.UriToCurie(commentId));
                writer.WriteEndElement();
            }

            if (LeapActiveTime != null && LeapActiveTime.Value.TotalMilliseconds > 0) {
                LeapActiveTime.WriteTo(writer, feed);
            }

            foreach (LeapDataItem data in this.PersonalData) {
                data.WriteTo(writer, "persondata", feed);
            }

            foreach (LeapDataItem data in this.OrganisationData) {
                data.WriteTo(writer, "orgdata", feed);
            }

            foreach (LeapDate date in this.LeapDates) {
                date.WriteTo(writer, feed);
            }

            foreach (LeapSpatial spatial in this.LeapSpatial) {
                spatial.WriteTo(writer, feed);
            }

            base.WriteElementExtensions(writer, version);
        }
    }
}
